package com.sky.templatehttpd;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.InvalidParameterException;
import java.util.Properties;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

class Request {

    /** Commons logger used for logging. */
    private static final Log LOG = LogFactory.getLog(Request.class);
    /** Buffer size for reading socket. */
    private static final int READBUFFER_SIZE = 2048;
    private static final int BASE_HEXA = 16;
    /** Method post header parameter containing content length. */
    private static final String POST_CONTENTLENGTH = "content-length";
    private static final String POST_REQUEST = "POST";
    private static final String HEADPARAM_ENTRYSEPARATOR = ":";
    private static final String URIPARAM_SEPARATOR = "&";
    private static final String URIPARAM_ENTRYSEPARATOR = "=";
    private String method;
    private String uri;
    private Properties header;
    private Properties params;

    Request(InputStream streamRequest) throws IOException {
        if (streamRequest == null) {
            throw new NullPointerException("Invalid input stream.");
        }

        BufferedReader requestReader = new BufferedReader(new InputStreamReader(streamRequest), READBUFFER_SIZE);
        // Read the request line (Method and URI plus optional unused Protocol)
        String requestLine = requestReader.readLine();
        if (requestLine == null || requestLine.length() == 0) {
            LOG.warn("No content on request. don't send any response.");
            return;
        }
        StringTokenizer requestParts = new StringTokenizer(requestLine);
        if (requestParts.countTokens() < 2) {
            throw new InvalidParameterException("Request syntax error. Usage: GET /path/file.htm [PROTOCOL]");
        }
        if (requestParts.countTokens() > 3) {
            LOG.warn("Invalid request line format, data are present after protocol, '" + requestLine + "'");
        }
        method = requestParts.nextToken();
        uri = requestParts.nextToken();
        // Decode parameters from the URI
        params = new Properties();
        int qmi = uri.indexOf('?');
        if (qmi >= 0) {
            decodeParams(uri.substring(qmi + 1), params);
            uri = decodePercent(uri.substring(0, qmi));
        } else {
            uri = decodePercent(uri);
        }
        // Request may be followed by HTTP headers.
        // NOTE: this now forces header names lowercase since they are case insensitive and vary by client.
        header = new Properties();
        String propertyLine;
        do {
            propertyLine = requestReader.readLine();
            if (propertyLine == null || propertyLine.trim().length() <= 0) {
                break;
            }
            String[] propertyEntry = propertyLine.split(HEADPARAM_ENTRYSEPARATOR, 2);
            if (propertyEntry.length != 2) {
                LOG.warn("Ignore invalid header property line '" + propertyLine + "'");
                continue;
            }
            header.put(propertyEntry[0].trim().toLowerCase(), propertyEntry[1].trim());
        } while (true);
        // If the method is POST, read post data.
        if (method.equalsIgnoreCase(POST_REQUEST)) {
            // They may be content-length property in data section.
            long lengthPost;
            try {
                String contentLength = header.getProperty(POST_CONTENTLENGTH);
                lengthPost = Integer.parseInt(contentLength);
            } catch (Exception ex) {
                lengthPost = Long.MAX_VALUE;
            }
            // Read Post properties
            StringBuilder postLine = new StringBuilder("");
            char[] readBuffer = new char[READBUFFER_SIZE];
            do {
                int readSize = requestReader.read(readBuffer, 0, READBUFFER_SIZE);
                if (readSize < 0 || lengthPost <= 0) {
                    break;
                }
                lengthPost -= readSize;
                String readContent = String.valueOf(readBuffer, 0, readSize);
                postLine.append(readContent);
                if (readContent.endsWith("\r\n") || lengthPost <= 0) {
                    break;
                }
            } while (true);

            decodeParams(postLine.toString().trim(), params);
        }
    }

    public String method() {
        return method;
    }

    public String uri() {
        return uri;
    }

    public Properties header() {
        return header;
    }

    public Properties params() {
        return params;
    }

    /**
     * Decodes a percent encoded string.<br/>
     * For example: "an+example%20string" -> "an example string".
     * @param str String to decode.
     * @return Decoded string.
     */
    private static String decodePercent(String str) {
        if (str == null) {
            return null;
        }

        try {
            StringBuilder decodedString = new StringBuilder();
            for (int strPos = 0; strPos < str.length(); ++strPos) {
                char c = str.charAt(strPos);
                if (c == '+') {
                    c = ' ';
                } else if (c == '%') {
                    char[] hexChar = new char[]{str.charAt(++strPos), str.charAt(++strPos)};
                    c = (char) Integer.parseInt(String.valueOf(hexChar), BASE_HEXA);
                }

                decodedString.append(c);
            }
            return decodedString.toString();

        } catch (Exception ex) {
            throw new InvalidParameterException("Request syntax error, Bad percent-encoding (" + ex.getMessage() + ")");
        }
    }

    /**
     * Decodes parameters from percent-encoded URI-format and adds them to given Properties.<br>
     * <i>NOTE</i>: this doesn't support multiple identical keys due to the simplicity of Properties,
     * if you need multiples, you might want to replace the Properties with a Hastable of Vectors or such.
     * @param params parameters string uri encoded (e.g. "name=Jack%20Daniels&pass=Single+Malt").
     * @param properties Properties where to append decoded params content.
     */
    private static void decodeParams(String params, Properties properties) {
        if (params == null) {
            return;
        }

        final StringTokenizer entries = new StringTokenizer(params, URIPARAM_SEPARATOR);
        while (entries.hasMoreTokens()) {
            final String entry = entries.nextToken();
            final String[] entryContent = entry.split(URIPARAM_ENTRYSEPARATOR, 2);
            if (entryContent.length != 2) {
                LOG.warn("Invalid parameter entry ('" + entry + "'). Usage key=value.");
                continue;
            }
            final String key = decodePercent(entryContent[0]).trim();
            final String value = decodePercent(entryContent[1]);
            properties.put(key, value);
        }
    }
}
